﻿using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using System.Xml;
using EnvDTE;

namespace SPDevExplorer
{
    public partial class SiteExplorer
    {
        private static string FullUrlOfNode(TreeNode node)
        {
            TreeNode siteNode = node;
            while (siteNode.Parent != null)
            {
                siteNode = siteNode.Parent;
            }

            // Now construct the full Url
            string fullUrl = siteNode.Text;
            if (!fullUrl.EndsWith("/"))
            {
                fullUrl += "/";
            }

            SharePointProperties sp = new SharePointProperties(node);
            fullUrl += sp.Url;

            return fullUrl;
        }

        public static string EnsurePath(TreeNode n)
        {
            TreeNode oCurrentNode = n;
            Stack<string> st = new Stack<string>();

            do
            {
                st.Push(
                    oCurrentNode.Text.Replace(":", "-").Replace("'", "").Replace("/", "").Replace("<", "").Replace("*", "").
                        Replace("\"", "").Replace("?", "").Replace("|", "").Replace(">", ""));
                if (oCurrentNode.Parent != null) oCurrentNode = oCurrentNode.Parent;
            } while (oCurrentNode.Parent != null);

            string sPath = ExplorerSettings.Current(n).WorkingFolder;

            while (st.Count > 0)
            {
                string s = st.Pop();
                if (!Directory.Exists(sPath + s))
                    Directory.CreateDirectory(sPath + s);

                sPath += s + "\\";
            }

            return sPath;
        }

        private void RecurseEnsureFiles(TreeNode treeNode, bool bOverwrite)
        {
            EnsureFilesInFolder(treeNode, bOverwrite);
            foreach (TreeNode n in treeNode.Nodes)
            {
                FolderProperties fp = new FolderProperties(n);
                if (fp.IsValidFolder && !fp.IsSystem) RecurseEnsureFiles(n, bOverwrite);
            }
        }

        private void ExpandSubNodes(TreeNode node)
        {
            foreach (TreeNode n in node.Nodes)
            {
                FolderProperties p = new FolderProperties(n);
                if (p.Xml.Name == "Folder")
                {
                    if (n.Nodes.Count == 0 /*(p.ItemCount > 0 || p.FolderCount > 0) && */)
                        LoadSubNodes(p, n);
                }
            }
        }

        private void LoadSubNodes(FolderProperties p, TreeNode n)
        {
            ListFoldersForNode(n, p.ID);
        }

        public void LoadSite(string url, string domain, string userName, string password)
        {
            try
            {
                // Always use lowercase url's, mixed case can get us in trouble when looking up existing url's
                url = url.ToLower();

                bool bServiceExists = _serviceManager.Exists(url);
                if (bServiceExists)
                {
                    MessageBox.Show(String.Format("A site with url '{0}' already exists in the SharePoint Explorer.", url), "Error");
                    return;
                }

                LogEvent(2, "Loading site: " + url);
                SPDevExplorerWebService.DevServices oServices = _serviceManager.Create(url, domain, userName, password);

                // Test if the correct webservice version is installed on the server
#if true
                try
                {
                    string webServiceVersionAsString = oServices.CurrentWebServiceVersion();
                    double webServiceVersion = Double.Parse(webServiceVersionAsString);
                    double requiredWebServiceVersion = Double.Parse(SPDevExplorer.AddInVersionNumber.RequiredWebServiceVersionNumber);
                    LogEvent(2, String.Format("Found webservice version: {0}, required webservice version: {1}", webServiceVersion, requiredWebServiceVersion));
                    if (requiredWebServiceVersion < webServiceVersion)
                    {
                        Uri siteUri = new Uri(url);
                        string serverUrl = siteUri.Host;
                        MessageBox.Show(
                            String.Format(
                                "The SPDevExplorer web service on server '{0}' has the SPDevExplorer web service with version {1} installed. This web service version is out of sync with the required web service version {2}. Please (re)install the SPDevExplorer.Solution.WSP package of SPDevExplorer version {2} on server '{0}'.",
                                serverUrl, webServiceVersion, SPDevExplorer.AddInVersionNumber.RequiredWebServiceVersionNumber), "Error");
                        return;
                    }
                }
                catch (Exception ex)
                {
                    Uri siteUri = new Uri(url);
                    string serverUrl = siteUri.Host;
                    MessageBox.Show(
                        String.Format("Failed to retrieve the current version of the SPDevExplorer web service on server '{0}' probably because you are not authorized to access the web service. The following exception is thrown: {1}", 
                            serverUrl, ex.Message), "Error");
                    return;
                }
#endif
                if (!bServiceExists)
                {
                    oServices.ListFoldersCompleted +=
                        new SPDevExplorerWebService.ListFoldersCompletedEventHandler(DocServices_ListFoldersCompleted);
                    oServices.GetFileCompleted +=
                        new SPDevExplorerWebService.GetFileCompletedEventHandler(DocServices_GetFileCompleted);
                    oServices.GetFileBytesCompleted +=
                        new SPDevExplorerWebService.GetFileBytesCompletedEventHandler(DocServices_GetFileBytesCompleted);
                }

                XmlDocument oDoc = new XmlDocument();
                _ctlExplorerTree.Tag = oDoc;
                oDoc.LoadXml("<Sites/>");
                // SvdO: Here the http:// is removed, but could be https:// as well, don't remove it, include it in the node for display
#if false
                TreeNode oRootNode = new TreeNode(Url.Substring(7), (int)NodeImages.Web, (int)NodeImages.Web);
#else
                TreeNode oRootNode = new TreeNode(url, (int) NodeImages.Web, (int) NodeImages.Web);
#endif
                // /SvdO

                oRootNode.ContextMenuStrip = ctxMenuSite;
                _ctlExplorerTree.Nodes.Add(oRootNode);

                LogEvent(2, "Calling web service: " + oServices.Url);
                ListFoldersForNode(oRootNode, "");
            }
            catch (Exception ex)
            {
                LogEvent(ex);
            }
        }

        private void DocServices_GetFileBytesCompleted(object sender,
                                                       SPDevExplorerWebService.GetFileBytesCompletedEventArgs e)
        {
            try
            {
                if (e.Error == null && e.UserState is TreeNode)
                {
                    TreeNode n = (TreeNode) e.UserState;

                    FileProperties p = new FileProperties(n);
                    FolderProperties fp = new FolderProperties(n.Parent);
                    string sPath = EnsurePath(n.Parent) + p.Name;

                    SaveBytesToWorkingFolderFile(p, fp, e.Result, sPath);
                }
                else
                    LogEvent(e.Error, false);
            }
            catch (Exception ex)
            {
                LogEvent(ex, false);
            }
        }

        private void DocServices_GetFileCompleted(object sender, SPDevExplorerWebService.GetFileCompletedEventArgs e)
        {
            try
            {
                if (e.Error == null && e.UserState is TreeNode)
                {
                    TreeNode n = (TreeNode) e.UserState;

                    FileProperties p = new FileProperties(n);
                    FolderProperties fp = new FolderProperties(n.Parent);
                    string sPath = EnsurePath(n.Parent) + p.Name;

                    SaveStringToWorkingFolderFile(p, fp, e.Result, sPath);
                }
                else
                    LogEvent(e.Error, false);
            }
            catch (Exception ex)
            {
                LogEvent(ex, false);
            }
        }

        private void DocServices_ListFoldersCompleted(object sender,
                                                      SPDevExplorerWebService.ListFoldersCompletedEventArgs e)
        {
            TreeNode oNode = (TreeNode) e.UserState;
            try
            {
                _nodesLoading.Remove(oNode);
                FlashNode(oNode, true);
                LogEvent(2, "ListFoldersCompleted: " + oNode.Text);

                if (e.Error == null)
                {
                    XmlDocument oDoc = new XmlDocument();
                    oDoc.LoadXml(e.Result);

                    XmlDocument oRootDoc = (XmlDocument) _ctlExplorerTree.Tag;
                    XmlElement oImported = (XmlElement) oRootDoc.ImportNode(oDoc.DocumentElement, true);

                    if (oNode.Tag == null)
                    {
                        oRootDoc.DocumentElement.AppendChild(oImported);
                        oNode.Tag = oImported;
                    }
                    else
                    {
                        XmlElement oCurrent = (XmlElement) oNode.Tag;
                        oNode.Tag = oImported;
                        oCurrent.InnerXml = oImported.InnerXml;
                    }

                    // SvdO: Webs gives warning already added, multiple webs with empty name... ignore now
                    //AddWebsToTree(oNode, oImported);
                    AddFoldersToTree(oNode, oImported);
                    AddFilesToTree(oNode, oImported);
                }
                else
                {
                    LogEvent(e.Error, false);
                    if (oNode.Parent == null)
                    {
                        // SvdO: node.Text now contains full url, don't prefix with http://
#if false
                        _serviceManager.Remove("http://" + oNode.Text);
#else
                        _serviceManager.Remove(oNode.Text);
#endif
                        // /SvdO
                        oNode.Remove();
                    }
                }
            }
            catch (Exception ex)
            {
                if (oNode.Parent == null)
                {
                    // SvdO: node.Text now contains full url, don't prefix with http://
#if false
                    _serviceManager.Remove("http://" + oNode.Text);
#else
                    _serviceManager.Remove(oNode.Text);
#endif
                    // /SvdO
                    oNode.Remove();
                }

                LogEvent(ex);
            }
        }

        /// <summary>
        /// Given a file tree node, retrieve the contents of the file from SharePoint
        /// and persist it in the working folder on the local filesystem
        /// </summary>
        /// <param name="fileNode">The file tree node to save to working folder</param>
        private void SaveFileFromSharePointToWorkingFolder(TreeNode fileNode)
        {
            FileProperties p = new FileProperties(fileNode);
            FolderProperties fp = new FolderProperties(fileNode.Parent);
            byte[] arFileBytes = DevServices(fileNode).GetFileBytes(p.Url);
            string sPath = FileNodeToWorkingFolderFilePath(fileNode);

            if (!fp.IsSystem)
            {
                RefreshFileDetails(fileNode);
            }

            // Save the file contents in the working folder on the filesystem
            SaveBytesToWorkingFolderFile(p, fp, arFileBytes, sPath);
        }

        /// <summary>
        /// Given a file tree node, retrieve the contents of the file from working folder
        /// and persist it to SharePoint without conversion (binary)
        /// </summary>
        /// <param name="fileNode">The file tree node to save to SharePoint</param>
        private void SaveFileFromWorkingFolderToSharePoint(TreeNode fileNode)
        {
            byte[] arFile = LoadWorkingFolderFileBytes(fileNode);

            // SaveFileBytes fails when an empty array is saved. If the length of the array is null, create an array of length 1 with '\0' in it
            if (arFile.Length == 0)
            {
                arFile = new byte[] {0};
            }

            FileProperties p = new FileProperties(fileNode);
            DevServices(fileNode).SaveFileBytes(p.Url, arFile);
        }

        /// <summary>
        /// Get the path to the file in the working folder
        /// </summary>
        /// <param name="fileNode">the file tree node of the file in the working folder</param>
        /// <returns>The opath to the file in th working folder</returns>
        private string FileNodeToWorkingFolderFilePath(TreeNode fileNode)
        {
            FileProperties p = new FileProperties(fileNode);
            string sPath = EnsurePath(fileNode.Parent) + p.Name;
            return sPath;
        }

        private void OpenFile(TreeNode fileNode)
        {
            string sPath = FileNodeToWorkingFolderFilePath(fileNode);

            FolderProperties fp = new FolderProperties(fileNode.Parent);
            if (!fp.IsSystem)
            {
                RefreshFileDetails(fileNode);
            }

            if (!DTE.ItemOperations.IsFileOpen(sPath, EnvDTE.Constants.vsViewKindAny))
            {
                SaveFileFromSharePointToWorkingFolder(fileNode);
                OpenWorkingFolderFileInVisualStudioOrApplication(fileNode);
            }
            else
            {
                Window w = _windows[sPath];
                w.Activate();
            }
        }

        /// 
        /// <summary>
        /// Load the bytes of a working folder file
        /// </summary>
        /// <param name="fileNode">the file tree node to load from working folder</param>
        /// <returns>The byte array containing the contents of the working folder file</returns>
        private byte[] LoadWorkingFolderFileBytes(TreeNode fileNode)
        {
            string sPath = FileNodeToWorkingFolderFilePath(fileNode);
            byte[] arFile = System.IO.File.ReadAllBytes(sPath);
            return arFile;
        }

        private void SaveBytesToWorkingFolderFile(FileProperties p, FolderProperties fp, byte[] arFile, string sPath)
        {
            if (File.Exists(sPath))
            {
                FileInfo fi = new FileInfo(sPath);
                fi.IsReadOnly = false;
            }

            System.IO.File.WriteAllBytes(sPath, arFile);


            if ((!p.HasCheckedOut && fp.ForceCheckout) || fp.ReadOnly)
            {
                System.IO.FileInfo fi = new FileInfo(sPath);
                fi.IsReadOnly = true;
            }
        }

        /// <summary>
        /// Save a string to a working folder file
        /// </summary>
        /// <param name="p">The properties of the file to save</param>
        /// <param name="fp">The properties of the folder containing the file to save</param>
        /// <param name="sFile">The text to save to file</param>
        /// <param name="sPath">The path to the file to save to</param>
        private void SaveStringToWorkingFolderFile(FileProperties p, FolderProperties fp, string sFile, string sPath)
        {
            if (File.Exists(sPath))
            {
                FileInfo fi = new FileInfo(sPath);
                fi.IsReadOnly = false;
            }

            System.IO.File.WriteAllText(sPath, sFile);


            if ((!p.HasCheckedOut && fp.ForceCheckout) || fp.ReadOnly)
            {
                System.IO.FileInfo fi = new FileInfo(sPath);
                fi.IsReadOnly = true;
            }
        }


        private void ListFoldersForNode(TreeNode oNode, string folderID)
        {
            if (_nodesLoading.Contains(oNode)) return;

            if (ParentNodeAfterRefresh == null)
            {
                // No parent node selected after refresh, make the specified node the expanded selected node
                ParentNodeAfterRefresh = oNode;
                TextOfNodeToMakeCurrentAfterRefresh = null;
            }

            _timer.Start();
            _nodesLoading.Add(oNode);
            DevServices(oNode).ListFoldersAsync(folderID, oNode);
        }

        private void ListFoldersForNode_Tick(object sender, EventArgs e)
        {
            try
            {
                foreach (TreeNode tn in _nodesLoading)
                {
                    FlashNode(tn, false);
                }

                if (_nodesLoading.Count == 0)
                {
                    _timer.Stop();
                    // done loading, expand node if current node, and search selected node before refresh
                    if (ParentNodeAfterRefresh != null)
                    {
                        // Assume parent node to become the current node
                        _ctlExplorerTree.SelectedNode = ParentNodeAfterRefresh;
                        ParentNodeAfterRefresh.Expand();
                        if (TextOfNodeToMakeCurrentAfterRefresh != null)
                        {
                            foreach (TreeNode node in ParentNodeAfterRefresh.Nodes)
                            {
                                if (node.Text == TextOfNodeToMakeCurrentAfterRefresh)
                                {
                                    _ctlExplorerTree.SelectedNode = node; // found the required node!

                                    // Execute the node action if configured
                                    if (node != null)
                                    {
                                        if (ActionOnNodeAfterRefresh != null)
                                        {
                                            ActionOnNodeAfterRefresh(node);
                                            ActionOnNodeAfterRefresh = null;
                                        }
                                    }
                                    break;
                                }
                            }
                        }
                        ParentNodeAfterRefresh = null;
                        TextOfNodeToMakeCurrentAfterRefresh = null;
                    }
                }
            }
            catch (Exception ex)
            {
                LogEvent(ex, false);
            }
        }

        private static void FlashNode(TreeNode oNode, bool clear)
        {
            if (clear)
            {
                oNode.ImageIndex = oNode.SelectedImageIndex;
                return;
            }

            if (oNode.ImageIndex == oNode.SelectedImageIndex)
                oNode.ImageIndex = 48;
            else if (oNode.ImageIndex == 36)
                oNode.ImageIndex = 48;
            else
                oNode.ImageIndex--;
        }

        // SvdO: sub web not supported for now, add as separate site
#if false
        private void AddWebsToTree(TreeNode oNode, XmlElement oImported)
        {
            try
            {
                XmlNodeList oList = oImported.SelectNodes("Web");
                SortedList<string, XmlElement> items = new SortedList<string, XmlElement>();

                foreach (XmlElement oWebXml in oList)
                    items.Add(oWebXml.GetAttribute("Name"), oWebXml);

                foreach (XmlElement oWebXml in items.Values)
                {
                    FileProperties p = new FileProperties(oWebXml);
                    int iImageIndex = FileImageIndex(p.Name);
                    TreeNode n = new TreeNode(p.Name, iImageIndex, iImageIndex);
                    oNode.Nodes.Add(n);
                    n.Tag = oWebXml;
                    ConfigureTreeNode(n);
                    n.ContextMenuStrip = ctxMenuFile;
                }
            }
            catch (Exception ex)
            {
                LogEvent(ex);
            }
        }
#endif

        /// <summary>
        /// Add file children to the given tree node sorted by name
        /// </summary>
        /// <param name="oNode">Parent node to add child files to</param>
        /// <param name="xmlElement">XML blob containing information on all child files</param>
        private void AddFilesToTree(TreeNode oNode, XmlElement xmlElement)
        {
            Debug.Assert(oNode != null);
            try
            {
                // Collect all "File" nodes from the XML blob, and add the file nodes to a sorted list
                XmlNodeList oList = xmlElement.SelectNodes("File");
                SortedList<string, XmlElement> items = new SortedList<string, XmlElement>();

                foreach (XmlElement oFileXml in oList)
                {
                    string filename = oFileXml.GetAttribute("Name");
                    items.Add(filename, oFileXml);
                }

                // Now process the file items in the sorted list and add them as child nodes to the given node
                foreach (XmlElement oFileXml in items.Values)
                {
                    FileProperties p = new FileProperties(oFileXml);
                    int iImageIndex = FileImageIndex(p.Name);
                    TreeNode n = new TreeNode(p.Name, iImageIndex, iImageIndex);
                    oNode.Nodes.Add(n);
                    n.Tag = oFileXml;
                    ConfigureTreeNode(n);
                    n.ContextMenuStrip = ctxMenuFile;
                }
            }
            catch (Exception ex)
            {
                LogEvent(ex);
            }
        }

        private void RefreshFileDetails(TreeNode fileNode)
        {
            FileProperties p = new FileProperties(fileNode);

            FolderProperties fp = new FolderProperties(fileNode.Parent);
            if (fp.IsSystem) return;

            string sResponse = DevServices(fileNode).RefreshFileDetails(p.Url);
            LogEvent(2, sResponse);
            ReplaceFileNode(p, sResponse);
        }

        private void ReplaceFileNode(FileProperties p, string sXML)
        {
            if (!string.IsNullOrEmpty(sXML))
            {
                XmlDocument oDoc = new XmlDocument();
                oDoc.LoadXml(sXML);
                XmlElement oImported = (XmlElement) p.Xml.OwnerDocument.ImportNode(oDoc.DocumentElement, true);
                p.Xml.ParentNode.ReplaceChild(oImported, p.Xml);
                _ctlExplorerTree.SelectedNode.Tag = oImported;
                ConfigureTreeNode(_ctlExplorerTree.SelectedNode);
            }
        }

        private static void ConfigureTreeNode(TreeNode node)
        {
            Debug.Assert(node != null, "ConfigureTreeNode: specified node in null");
            Debug.Assert(node.TreeView != null, String.Format("ConfigureTreeNode: node '{0}' has no connected TreeView (TreeView is null)", node.Text));

            FileProperties file = new FileProperties(node);
            XmlElement oParent = (XmlElement)file.Xml.ParentNode;

            // SvdO: This code makes visualization of checked out files in root of site not work
            //if (oParent.GetAttribute("ForceCheckout") == "False") return;

            node.TreeView.Invalidate();
            node.StateImageIndex = -1;

            if (file.HasCheckedOut)
            {
                node.StateImageKey = "Edit";
                node.ToolTipText = "Checked out to you";
                return;
            }

            if (file.CheckedOutBy != "")
            {
                node.StateImageIndex = 3;
                node.ToolTipText = "Checked out by " + file.CheckedOutBy;
                return;
            }

            // SvdO: Show draft icon if no published version available
            if (file.IsInDraft)
            {
                node.StateImageKey = "Draft";
                node.ToolTipText = "Latest visible version is a draft version. It is not published, or not approved yet.";

                if (file.ModerationStatus == "Pending")
                {
                    node.StateImageKey = "Pending";
                    node.ToolTipText = "Latest visible version is pending for approval.";
                }

                return;
            }
        }

        private int FileImageIndex(string p)
        {
            string sType = p.Substring(p.LastIndexOf(".") + 1);
            switch (sType)
            {
                case "aspx":
                    return (int) NodeImages.aspx;
                case "master":
                    return (int) NodeImages.master;
                case "js":
                    return (int) NodeImages.js;
                case "xsl":
                case "xslt":
                case "xml":
                    return (int) NodeImages.xslt;
                case "bmp":
                case "gif":
                case "png":
                case "jpg":
                case "jpeg":
                    return (int) NodeImages.image;
                case "doc":
                    return (int) NodeImages.doc;
                case "css":
                    return (int) NodeImages.css;
            }

            return (int) NodeImages.File;
        }

        private void AddFoldersToTree(TreeNode oRootNode, XmlElement xmlElement)
        {
            // Collect all "Folder" nodes from the XML blob, and add the folder nodes to a sorted list
            XmlNodeList oList = xmlElement.SelectNodes("Folder");
            SortedList<string, XmlElement> items = new SortedList<string, XmlElement>();

            foreach (XmlElement oFolderXml in oList)
            {
                string foldername = oFolderXml.GetAttribute("Name");
                items.Add(foldername, oFolderXml);
            }

            // Now process the folder items in the sorted list and add them as child nodes to the given node
            foreach (XmlElement oFolderXml in items.Values)
            {
                // SvdO: if _displayAllFoldersAndFilesInTree is true, ignore the list of hidden folders
                if (DisplayAllFoldersAndFilesInTree || !IsHidden(oFolderXml))
                {
                    int iImage = (int) NodeImages.Folder;

                    if (oFolderXml.GetAttribute("IsDocumentLibrary") == bool.TrueString)
                    {
                        iImage = (int)NodeImages.DocLibrary;
                    }
                    if (oFolderXml.GetAttribute("System") == bool.TrueString)
                    {
                        iImage = (int)NodeImages.System;
                    }

                    string foldername = oFolderXml.GetAttribute("Name");
                    TreeNode n = new TreeNode(foldername, iImage, iImage);
                    n.NodeFont = new Font(this.Font, FontStyle.Regular);
                    oRootNode.Nodes.Add(n);
                    n.Tag = oFolderXml;
                    n.ContextMenuStrip = ctxMenuFolder;

                    // Recurse to add sub folders and files
                    AddFoldersToTree(n, oFolderXml);
                    AddFilesToTree(n, oFolderXml);
                }
            }
        }

        private bool IsHidden(XmlElement xmlElement)
        {
            if (btnShowHidden.Checked) return false;
            FolderProperties fp = new FolderProperties(xmlElement);
            switch (fp.Name.ToLower())
            {
                case "forms":
                case "_wp":
                case "_cts":
                case "cache profiles":
                case "long running operation status":
                case "_private":
                case "_vti_pvt":
                case "iwconvertedforms":
                case "quick deploy items":
                case "reusablecontent":
                case "notification pages":
                case "pages":
                case "workflowtasks":
                case "relationships list":
                case "reports list":
                case "formservertemplates":
                case "wt":
                case "variation labels":
                case "users":
                case "lists":
                    return true;
            }

            return false;
        }
    }
}